Method Chaining (Fluent) topic
How to use Fluent Operators
fluent_operator.dart is a part of package:cell_flow/cell_flow.dart. One import is enough:
import 'package:cell_flow/cell_flow.dart';
You do not import the extension file yourself.
The extensions wrap the same Flow.* static factories. Behaviour does not live in two places.
| Extension | On | Use it to |
|---|---|---|
CellFlowOperators |
Cell |
start a pipeline |
FlowOperators |
FlowHandle |
keep chaining via .cell |
Nested vs fluent
// Nested factories
final out = Flow.map<int, String>(
Flow.filter<int>(clicks.cell, test: (n) => n > 0).cell,
project: (n) => '$n',
);
// Same pipeline, chained
final out = clicks.cell
.filter<int>(test: (n) => n > 0)
.map<int, String>(project: (n) => '$n');
Observe the last handle’s cell:
Cell.observe(
source: out.cell,
effect: (Pulse p) => print(p.payload),
);
await clicks.emitAsync(3);
Start from a Cell
CellFlowOperators is the on-ramp. Typical sources:
final ingress = Cell.ingress<int>();
ingress.cell.filter<int>(test: (n) => n.isEven);
ingress.cell.map<int, String>(project: (n) => '#$n');
ingress.cell.tap<int>(onValue: print);
// One-shot sequences (first pulse on this cell arms them)
arm.cell.of<String>(values: ['a', 'b']);
arm.cell.fromIterable<int>(iterable: [1, 2, 3]);
arm.cell.range(start: 1, count: 5);
arm.cell.fromFuture<User>(future: api.me());
arm.cell.fromStream<int>(stream: ticker);
Anything not on Cell can start with a factory, then chain:
Flow.debounce<String>(query.cell, duration: const Duration(milliseconds: 300))
.asyncMapLatest<String, List<Hit>>(mapper: api.search);
Practical pipelines
Search-as-you-type
final query = Cell.ingress<String>();
final results = query.cell
.map<String, String>(project: (q) => q.trim())
.filter<String>(test: (q) => q.length >= 2)
.debounce<String>(duration: const Duration(milliseconds: 300))
.asyncMapLatest<String, List<Hit>>(mapper: (q) => api.search(q))
.tap<List<Hit>>(onValue: (hits) => analytics.track('search', hits.length));
asyncMapLatest drops a stale in-flight request when the user types again.
Form field → validated model
final raw = Cell.ingress<String>();
final email = raw.cell
.map<String, String>(project: (s) => s.trim().toLowerCase())
.filter<String>(test: (s) => s.contains('@'))
.distinct<String>()
.asyncMap<String, User>(
mapper: (e) => api.lookup(e),
onError: (e, _) => log.warn(e),
);
Scroll / pointer rate limit
final offsets = Cell.ingress<double>();
final sampled = offsets.cell
.throttle<double>(
duration: const Duration(milliseconds: 16),
leading: true,
trailing: true,
)
.map<double, int>(project: (y) => y.round());
Button → HTTP, ignore double-taps
final taps = Cell.ingress<void>();
final saved = taps.cell.exhaustMap<void, Receipt>(
project: (_) => api.checkout(cart),
);
While checkout is running, extra taps are ignored.
Latest of two Cells when the source moves
final price = Cell.ingress<int>();
final currency = Cell.ingress<String>();
final line = price.cell.withLatestFrom<int, String>(
others: [currency.cell],
combine: (p, latest) => '${latest.single} $p',
);
1 then 'EUR' then 2 → "EUR 2" (currency must already have a value).
Pair by index (not latest)
final ids = Cell.ingress<int>();
final names = Cell.ingress<String>();
final rows = ids.cell.zipWith<List<Object?>>(
others: [names.cell],
);
// 1, then 'Ada', then 2, then 'Bob' → [1, Ada], [2, Bob]
Batch telemetry
final events = Cell.ingress<Metric>();
final batches = events.cell
.bufferWithTimeAndCount<Metric>(
duration: const Duration(seconds: 5),
count: 50,
)
.asyncMap<List<Metric>, void>(mapper: (batch) => api.flush(batch));
Flush when 50 events arrive or 5 seconds pass, whichever first.
Paginated list from a stream of pages
final pages = Cell.ingress<List<Row>>();
final rows = pages.cell.concatAll<Row>();
// [a, b] then [c] → a, b, c in that order
Group a live feed
final orders = Cell.ingress<Order>();
final byStatus = orders.cell.groupBy<Order, String>(
keyOf: (o) => o.status,
);
// payload is Grouped('paid', order)
Method catalogue
Payload type parameters (S, T, …) are the same as on Flow.*.
Start (Cell)
| Method | Rx analogue |
|---|---|
of / fromIterable / range / repeat |
of / from / range |
fromFuture / fromStream |
from(Future) / from(Stream) |
map / filter / tap |
start a transform |
Transform (FlowHandle)
| Method | Role |
|---|---|
map / mapTo / mapWithIndex / mapNotNull / mapWhen |
project |
pluck / pluckOr / pluckPath |
field access |
scan / reduce / pairwise |
running state |
Async
| Method | Concurrency |
|---|---|
asyncMap |
queue (one result each) |
asyncMapConcurrent |
overlap |
asyncMapLatest |
drop in-flight |
asyncExpand |
flatten inners, queued |
asyncFold |
async running fold |
Filter / take / skip
filter, take / takeWhile / takeUntil, skip / skipWhile / skipUntil / skipRepeated, distinct.
Flatten
concatMap, concatAll, mergeMap, switchMap, exhaustMap.
Combine
mergeWith, mergeAll, zipWith, combineLatestWith, withLatestFrom.
Time
delay / delayWhen, debounce, throttle, sample / sampleTime, auditTime, timeout, interval.
Collect / route / control
bufferCount / bufferTime, windowCount, groupBy, partition, startWith, share / shareReplay, retry, tap / tapAll / tapWithIndex.
Factories that exist on Flow but not on the handle (for example Flow.race, Flow.audit, Flow.windowTime) can still be inserted mid-pipeline:
Flow.race<int>(gate.cell, competitors: [a.future, b.future])
.tap<int>(onValue: print);
Rules that bite in real apps
- Inject with
IngressHandle.emitAsync, not a rawCell.emitAsync. - A Cell does not complete.
concatof two Cells is not “A then B”. Concatenate inners (lists / futures / streams) instead. combineLatest/withLatestFromwait until every other Cell has a value. Zip waits for the same index on every lane.- Time operators use real
Timers. Tests mustawaitpast the duration. - Type arguments are required on most methods (
filter<int>,map<int, String>). The handle itself is not generic in the facade. - Errors go to the
onError:you pass through. They are not rethrown into the chain unless that instruction emits an error pulse.
When to stay with Flow.*
Use the static factory when:
- you are arming a one-shot (
Flow.of,Flow.range) from a dedicated gate Cell - the operator is not on the extension yet
- you want the call site to name the operator for logs / codegen
Fluent and static can mix in the same pipeline. Both call the same instruction classes.
Classes
- Flow Features Architecture Walkthroughs Demo · ICU Alarm Pipeline Demo · Pharmacy Transactions Demo · Search Stability Demo · Forensic Pipeline HowTo-FlowInstructions Method Chaining (Fluent)
- The Transcription Orchestrator for the Cell framework, responsible for converting (transcribing) external stimuli, persistent state, and logic into reactive Cell topographies.